package org.springblade.resource.core;

import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springblade.common.entity.MinioPolicy;
import org.springblade.core.oss.model.OssFile;
import org.springblade.core.oss.props.OssProperties;
import org.springblade.core.oss.rule.OssRule;
import org.springblade.core.tool.utils.Func;
import org.springblade.resource.entity.OssFileOrDir;
import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
import org.springframework.web.multipart.MultipartFile;

import java.sql.Date;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@RequiredArgsConstructor
public class MinioTemplate {
	private final MinioClient minioClient;
	private final OssProperties ossProperties;
	private final OssRule ossRule;
	private final MultipartProperties multipartProperties;

	/*
			 储存桶操作
	 * */

	/**
	 * 创建储存桶
	 *
	 * @param bucketName 储存桶名称
	 */
	@SneakyThrows
	public void makeBucket(String bucketName) {
		if (!bucketExists(bucketName)) {
			minioClient.makeBucket(MakeBucketArgs.builder().bucket(getBucketName(bucketName)).build());
		}
	}

	/**
	 * 获取所有储存桶
	 *
	 * @return List<Bucket>
	 */
	@SneakyThrows
	private List<Bucket> listBuckets() {
		return minioClient.listBuckets();
	}

	/**
	 * 获取所有储存桶名字
	 *
	 * @return List<String>
	 */
	@SneakyThrows
	public List<String> listBucketNames() {
		return listBuckets().stream().map(Bucket::name).collect(Collectors.toList());
	}

	/**
	 * 验证是否有储存桶
	 *
	 * @param bucketName 存储桶名称
	 * @return boolean
	 */
	@SneakyThrows
	public boolean bucketExists(String bucketName) {
		return minioClient.bucketExists(BucketExistsArgs.builder().bucket(getBucketName(bucketName)).build());
	}

	/**
	 * 删除储存桶
	 *
	 * @param bucketName 存储桶名称
	 */
	@SneakyThrows
	public void removeBucket(String bucketName) {
		minioClient.removeBucket(RemoveBucketArgs.builder().bucket(getBucketName(bucketName)).build());
	}


	/*
			  文件操作
			  文件操作bucketName为空的话使用默认储存桶
	 * */


	/**
	 * 上传文件——通过文件流
	 *
	 * @param fileName 文件名称
	 * @param file     上传文件流
	 * @return OssFile
	 */
	@SneakyThrows
	public OssFile putFile(String fileName, MultipartFile file) {
		return putFile(null, fileName, file);
	}

	/**
	 * 上传文件——通过文件流
	 *
	 * @param bucketName 存储桶名称
	 * @param fileName   文件名称
	 * @param file       上传文件流
	 * @return OssFile
	 */
	@SneakyThrows
	public OssFile putFile(String bucketName, String fileName, MultipartFile file) {
		String newBucketName = getBucketName(bucketName);
		makeBucket(newBucketName);
		String newFileName = getFileName(fileName);
		minioClient.putObject(PutObjectArgs.builder()
			.bucket(newBucketName).object(newFileName).stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build());
		return statFile(bucketName, newFileName);
	}

	/**
	 * 获取上传文件链接——直链上传
	 *
	 * @param fileName 文件名称
	 * @param expiry   链接过期时间
	 * @return 上传文件链接
	 */
	@SneakyThrows
	public String putFileLink(String fileName, Integer expiry) {
		return putFileLink(null, fileName, expiry);
	}

	/**
	 * 获取上传文件链接——直链上传
	 *
	 * @param bucketName 存储桶名称
	 * @param fileName   文件名称
	 * @param expiry     过期时间（秒）
	 * @return 上传文件链接
	 */
	@SneakyThrows
	public String putFileLink(String bucketName, String fileName, Integer expiry) {
		return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.
			builder().method(Method.PUT).bucket(getBucketName(bucketName)).object(getFileName(fileName)).expiry(expiry).build());
	}

	/**
	 * 获取post策略(默认10分钟)
	 *
	 * @param fileName    文件名
	 * @param contentType 文件类型前缀
	 * @return Map<String, String>
	 */
	@SneakyThrows
	public MinioPolicy getPolicy(String fileName, String contentType) {
		return getPolicy(fileName, contentType, 10);
	}

	/**
	 * 获取post策略
	 *
	 * @param fileName    文件名
	 * @param contentType 文件类型前缀
	 * @param Minutes     过期时间
	 * @return Map<String, String>
	 */
	@SneakyThrows
	public MinioPolicy getPolicy(String fileName, String contentType, long Minutes) {
		return getPolicy(null, fileName, contentType, Minutes);
	}

	/**
	 * 获取post策略签名
	 *
	 * @param bucketName  储存桶
	 * @param fileName    文件名
	 * @param contentType 文件类型
	 * @param Minutes     过期时间
	 * @return Map<String, String>
	 */
	@SneakyThrows(Exception.class)
	public MinioPolicy getPolicy(String bucketName, String fileName, String contentType, long Minutes) {
		fileName = getFileName(fileName);
		bucketName = getBucketName(bucketName);
		PostPolicy policy = new PostPolicy(bucketName, ZonedDateTime.now().plusMinutes(Minutes));
		policy.addEqualsCondition("key", fileName);
		policy.addStartsWithCondition("Content-Type", contentType);
		policy.addContentLengthRangeCondition(0, Math.toIntExact(multipartProperties.getMaxFileSize().toBytes()));
		Map<String, String> formData = minioClient.getPresignedPostFormData(policy);
		return MinioPolicy.builder()
			.policy(formData.get("policy"))
			.xAmzDate(formData.get("x-amz-date"))
			.xAmzSignature(formData.get("x-amz-signature"))
			.xAmzAlgorithm(formData.get("x-amz-algorithm"))
			.xAmzCredential(formData.get("x-amz-credential"))
			.key(fileName)
			.postUrl(ossProperties.getEndpoint() + "/" + bucketName)
			.build();
	}

	/**
	 * 复制文件
	 *
	 * @param fileName     文件名称
	 * @param destFileName 文件名称
	 */
	@SneakyThrows
	public void copyFile(String fileName, String destFileName) {
		copyFile(null, fileName, null, destFileName);
	}

	/**
	 * 复制文件
	 *
	 * @param fileName       文件名称
	 * @param destBucketName 存储桶名称
	 * @param destFileName   文件名称
	 */
	@SneakyThrows
	public void copyFile(String fileName, String destBucketName, String destFileName) {
		copyFile(null, fileName, destBucketName, destFileName);
	}

	/**
	 * 复制文件
	 *
	 * @param bucketName     存储桶名称
	 * @param fileName       文件名称
	 * @param destBucketName 存储桶名称
	 * @param destFileName   文件名称
	 */
	@SneakyThrows
	public void copyFile(String bucketName, String fileName, String destBucketName, String destFileName) {
		CopySource.Builder source = CopySource.builder();
		if (Func.isNotEmpty(destBucketName)) {
			source.bucket(getBucketName(destBucketName));
		}
		if (Func.isNotEmpty(destFileName)) {
			source.object(destFileName);
		}
		minioClient.copyObject(CopyObjectArgs.builder()
			.bucket(getBucketName(bucketName)).object(fileName).source(source.build()).build());
	}

	/**
	 * 获取文件信息
	 *
	 * @param fileName 文件名称
	 * @return OssFile
	 */
	@SneakyThrows
	public OssFile statFile(String fileName) {
		return statFile(null, fileName);
	}

	/**
	 * 获取文件信息
	 *
	 * @param bucketName 存储桶名称
	 * @param fileName   文件名称
	 * @return OssFile
	 */
	@SneakyThrows
	public OssFile statFile(String bucketName, String fileName) {
		StatObjectResponse stat = minioClient.statObject(StatObjectArgs.builder().bucket(getBucketName(bucketName)).object(fileName).build());
		OssFile ossFile = new OssFile();
		ossFile.setName(fileName);
		ossFile.setLink(publicFileLink(bucketName, ossFile.getName()));
		ossFile.setHash(stat.etag());
		ossFile.setLength(stat.size());
		ossFile.setPutTime(Date.from(stat.lastModified().toInstant()));
		ossFile.setContentType(stat.contentType());
		return ossFile;
	}

	/**
	 * 获取文件链接(默认7天)
	 *
	 * @param fileName 文件名称
	 * @return 文件链接
	 */
	@SneakyThrows
	public String fileLink(String fileName) {
		return fileLink(fileName, 60 * 60 * 24 * 7);
	}

	/**
	 * 获取文件链接
	 *
	 * @param fileName 文件名称
	 * @return 文件链接
	 */
	@SneakyThrows
	public String fileLink(String fileName, Integer expiry) {
		return fileLink(null, fileName, expiry);
	}

	/**
	 * 获取文件链接
	 *
	 * @param bucketName 存储桶名称
	 * @param fileName   文件名称
	 * @param expiry     过期时间（秒）
	 * @return 文件链接
	 */
	@SneakyThrows
	public String fileLink(String bucketName, String fileName, Integer expiry) {
		return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs
			.builder().method(Method.GET).bucket(getBucketName(bucketName)).object(fileName).expiry(expiry).build());
	}

	/**
	 * 获取储存桶下的所有文件(递归查找)
	 *
	 * @return OssFile
	 */
	@SneakyThrows
	public List<OssFile> listAllObjects() {
		return listAllObjects(null);
	}

	/**
	 * 获取储存桶下的所有文件(递归查找)
	 *
	 * @param bucketName 存储桶名称
	 * @return OssFile
	 */
	@SneakyThrows
	public List<OssFile> listAllObjects(String bucketName) {
		Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder().bucket(getBucketName(bucketName)).recursive(true).build());
		List<OssFile> files = new ArrayList<>();
		for (Result<Item> next : results) {
			Item item = next.get();
			OssFile ossFile = new OssFile();
			ossFile.setName(item.objectName());
			ossFile.setLink(publicFileLink(bucketName, ossFile.getName()));
			ossFile.setHash(item.etag());
			ossFile.setLength(item.size());
			ossFile.setPutTime(Date.from(item.lastModified().toInstant()));
			files.add(ossFile);
		}
		return files;
	}

	/**
	 * 根据前缀获取文件和目录
	 *
	 * @param prefix 文件前缀
	 * @return OssFileOrDir
	 */
	@SneakyThrows
	public List<OssFileOrDir> listObjects(String prefix) {
		return listObjects(prefix);
	}

	/**
	 * 根据前缀获取文件和目录
	 *
	 * @param bucketName 存储桶名称
	 * @param prefix     文件前缀
	 * @return OssFileOrDir
	 */
	@SneakyThrows
	public List<OssFileOrDir> listObjects(String bucketName, String prefix) {
		ListObjectsArgs.Builder bucket = ListObjectsArgs.builder().bucket(getBucketName(bucketName));
		if (Func.isNotEmpty(prefix)) {
			bucket.prefix(prefix);
		}
		Iterable<Result<Item>> results = minioClient.listObjects(bucket.build());
		List<OssFileOrDir> ossFileOrDirs = new ArrayList<>();
		for (Result<Item> next : results) {
			Item item = next.get();
			OssFileOrDir ossFile = new OssFileOrDir();
			if (item.isDir()) {
				ossFile.setName(item.objectName());
				ossFile.setDir(true);
			} else {
				ossFile.setName(item.objectName());
				ossFile.setLink(publicFileLink(bucketName, ossFile.getName()));
				ossFile.setHash(item.etag());
				ossFile.setLength(item.size());
				ossFile.setPutTime(Date.from(item.lastModified().toInstant()));
				ossFile.setDir(false);
			}
			ossFileOrDirs.add(ossFile);
		}
		return ossFileOrDirs;
	}

	/*
			文件删除
	 */

	/**
	 * 删除文件
	 *
	 * @param fileName 文件名
	 */
	@SneakyThrows
	public void removeFile(String fileName) {
		removeFile(null, fileName);
	}

	/**
	 * 删除文件
	 *
	 * @param bucketName 储存桶
	 * @param fileName   文件名
	 */
	@SneakyThrows
	public void removeFile(String bucketName, String fileName) {
		minioClient.removeObject(RemoveObjectArgs.builder().bucket(getBucketName(bucketName)).object(fileName).build());
	}

	/**
	 * 批量删除
	 *
	 * @param fileNames 文件名组
	 */
	@SneakyThrows
	public void removeFiles(List<String> fileNames) {
		removeFiles(null, fileNames);
	}

	/**
	 * @param bucketName 储存桶
	 * @param fileNames  文件名组
	 */
	@SneakyThrows
	public void removeFiles(String bucketName, List<String> fileNames) {
		List<DeleteObject> objects = fileNames.stream().map(DeleteObject::new).collect(Collectors.toList());
		Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(getBucketName(bucketName)).objects(objects).build());
		for (Result<DeleteError> result : results) {
			DeleteError error = result.get();
			log.error("Error in deleting object " + error.objectName() + "; " + error.message());
		}
	}


	/**
	 * 获取文件公开链接
	 *
	 * @param fileName 文件名
	 * @return 文件公开链接
	 */
	public String publicFileLink(String fileName) {
		return publicFileLink(null, fileName);
	}

	/**
	 * 获取文件公开链接
	 *
	 * @param bucketName 储存桶
	 * @param fileName   文件名
	 * @return 文件公开链接
	 */
	public String publicFileLink(String bucketName, String fileName) {
		return String.format("%s/%s/%s", ossProperties.getEndpoint(), getBucketName(bucketName), fileName);
	}


	/**
	 * 根据规则生成存储桶名称规则
	 *
	 * @param bucketName 存储桶名称
	 * @return String
	 */
	private String getBucketName(String bucketName) {
		if (Func.isEmpty(bucketName)) {
			bucketName = ossProperties.getBucketName();
		}
		return ossRule.bucketName(bucketName);
	}

	/**
	 * 根据规则生成文件名称规则
	 *
	 * @param originalFilename 原始文件名
	 * @return string
	 */
	private String getFileName(String originalFilename) {
		return ossRule.fileName(originalFilename);
	}
}
